# G6.js 性能优化

# behavior

# drag-canvas

拖动会频繁触发updateViewport

// 去除无用逻辑 + 使用定时器,类nextTick原理去减少translate操作
// 实测setTimeout比requestAnimationFrame流畅一点
updateViewport(e) {
  if (!this.queue) {
    this.queue = [];
  }
  this.queue.push(() => {
    const origin = this.origin;
    const clientX = +e.clientX;
    const clientY = +e.clientY;
    if (isNaN(clientX) || isNaN(clientY) || !origin) {
      return;
    }
    const dx = clientX - origin.x;
    const dy = clientY - origin.y;
    this.origin = {
      x: clientX,
      y: clientY
    };
    this.graph.translate(dx, dy);
  });
  if (!this.doing) {
    this.doing = true;
    this.timer = setTimeout(() => {
      this.doing = false;
      this.queue.pop()();
      this.queue = [];
      clearTimeout(this.timer);
      this.timer = null;
    });
  }
}

# zoom

项目需要在缩放时改变显示的图片,

首先做了200ms的防抖

首次获取节点后缓存到graph实例上,避免每次都去取。在更新数据时要记得清除缓存

data.nodes = graph.getNodes();
data.edges = graph.getEdges();
data.combos = graph.getCombos();

图片根据缩放有几个档次,档次没变化时不对元素派发缩放事件

分批派发事件,截取指定数量的元素异步一批一批的派发事件。

# shape

# nodes

# 改变keyShape

开启优化enableOptimize后,拖动时只会显示keyshape。

但我们存在不同缩放比例时显示不同形状的需求,这里就需要动态的改变keyshape。

keyShape的实例上有个变量isKeyShape记录,在需要的时机改变对应shape的isKeyShape即可。

# 边颜色问题

因为画布是整个进行缩放的,所以在很小的时候,1px的边显示上是小于1px的,导致视觉上颜色变浅。

# 解决方案

在缩放的回调中,动态改变边的宽度。

当缩放比例小于1时,将需要显示的宽度除以当前的缩放比例,即将宽度放大了。

这里要给个最大值,不然有些本身就粗的线会变的很粗。

const needWidth = group.lineStatusConfig.statusInfo[configKey].width;
let showWidth = needWidth;
if (this.zoom && this.zoom < 1) {
  showWidth = needWidth / this.zoom;
  if (showWidth > 4) {
    showWidth = 4;
  }
}

# 节点位置整理

# 拖动自动吸附

G6提供了snapline 辅助线插件,但是居然不会自动吸附,所以需要自己拓展一下

# 原理

addAlignLine画完辅助线之后,根据辅助线的配置,找到最近的两条线的偏移量,改变拖动中的元素的位置即可,这里做的比较简单。

# 源码

/**
 * 自动吸附
 */
autoAdsorbFn(alignCfg) {
  const offset = {};
  const graph = this.get('graph');
  if (alignCfg) {
    const { verticals, horizontals } = alignCfg;
    const model = this.dragItem.getModel();
    const updateModel = {};
    if (verticals && verticals[0]) {
      offset.x = verticals[0].dis;
      updateModel.x = model.x + offset.x;
    }
    if (horizontals && horizontals[0]) {
      offset.y = horizontals[0].dis;
      updateModel.y = model.y + offset.y;
    }
    if (Object.keys(updateModel).length) {
      graph.updateItem(
        this.dragItem,
        updateModel
      );
    }
  }
  // 记录偏移量,在drag-end后更新位置的地方要加上
  graph.set('autoAdsorbOffset', offset);
}

需要注意的是,在拖动结束后,会发现位置的偏移。这是因为吸附是我们手动移的位置,dragend那获取到的坐标是鼠标的坐标。所以在做吸附偏移后,需记录上偏移量,在dragend的改变节点位置的操作中加上偏移量即可。

# 对齐功能

G6没提供选中几个节点去做对齐的功能,这里自己拓展一下。

主要做了一下几种对齐方式:

  • 左对齐
  • 右对齐
  • 水平居中对齐
  • 顶部对齐
  • 底部对齐
  • 垂直居中对齐
  • 水平等间距对齐
  • 垂直等间距对齐

# 上下左右对齐

直接找出最小的值,全部给那个值即可

# 水平、垂直中心对齐

找出两侧的值,求中心点即可

# 水平、垂直 等间距排序

间距 = (总长度 - 节点总长度) / (节点数 - 1

再从小的开始,改变每一个的坐标

坐标 += 间距 + 前一个节点的长度

ps:长度指宽或者高

# 源码

/**
 * 节点对齐操作
 */

// 更新的数据
let updateModels = {};
// 旧数据
let originModels = {};

/**
 * 获取节点数据
 * @param selectItem
 * @returns {*}
 */
function getNodes(selectItem) {
  return selectItem.nodes.map(item => {
    const model = item.getModel();
    return {
      node: item,
      model: model,
      x: model.x,
      y: model.y
    };
  });
}

/**
 * 获取极值
 * @param type
 * @param key  x,y
 * @param selectItem
 */
function findTarget(type, key, selectItem) {
  let target = selectItem[0][key];
  for (let i = 1; i < selectItem.length; i++) {
    const item = selectItem[i][key];
    switch (type) {
      case 'l':
      case 't':
        if (item < target) {
          target = item;
        }
        break;
      case 'r':
      case 'b':
        if (item > target) {
          target = item;
        }
        break;
    }
  }
  return target;
}

/**
 * 上下左右对齐
 * @param graph
 * @param type
 * @param selectItem
 * @param params
 */
function alignNormal (graph, type, selectItem, params) {
  const { positionKey } = params;
  const target = findTarget(type, positionKey, selectItem);

  selectItem.forEach(item => {
    updateItem(graph, item, positionKey, target);
  });
}

/**
 * 水平、垂直中心对齐
 * @param graph
 * @param type
 * @param selectItem
 * @param params
 */
function alignCenter(graph, type, selectItem, params) {
  // 找出x左右取中间值
  const { positionKey, minType, maxType } = params;
  const min = findTarget(minType, positionKey, selectItem);
  const max = findTarget(maxType, positionKey, selectItem);
  const mid = (min + max) / 2;

  selectItem.forEach(item => {
    updateItem(graph, item, positionKey, mid);
  });
}

/**
 * 水平、垂直 平均间隙排序
 * @param graph
 * @param type
 * @param selectItem
 * @param params
 * @returns {{updateModels: {}, originModels: {}}}
 */
function alignGap(graph, type, selectItem, params) {
  const { length } = selectItem;

  // 小于3不需要动
  if (length < 3) {
    return;
  }

  const { positionKey, lengthKey } = params;

  // 小的在前面
  selectItem.sort((a, b) => {
    return a[positionKey] - b[positionKey];
  });

  // 设备总长度
  let totalItemLength = 0;
  selectItem.forEach(item => {
    item[lengthKey] = item.node.getCanvasBBox()[lengthKey];
    totalItemLength += item[lengthKey];
  });

  const min = selectItem[0][positionKey];
  const max = selectItem[length - 1][positionKey] + selectItem[length - 1][lengthKey];
  // 选中的总长度
  const totalLength = max - min;
  // 节点间间距
  const gap = (totalLength - totalItemLength) / (selectItem.length - 1);

  // 每个节点要给的位置
  let position = min;
  for (let i = 1; i < length - 1; i++) {
    const item = selectItem[i];
    position += gap + selectItem[i - 1][lengthKey];
    updateItem(graph, item, positionKey, position);
  }
}

/**
 * 更新节点属性
 * @param graph
 * @param item
 * @param positionKey
 * @param newValue
 */
function updateItem(graph, item, positionKey, newValue) {
  const id = item.model.id;
  updateModels[id] = {
    [positionKey]: newValue
  };
  originModels[id] = {
    [positionKey]: item[positionKey]
  };
  graph.updateItem(
    item.node,
    updateModels[id]
  );
}


const ALIGN_TYPE = {
  l: {
    fn: alignNormal,
    params: {
      positionKey: 'x'
    }
  },
  r: {
    fn: alignNormal,
    params: {
      positionKey: 'x'
    }
  },
  t: {
    fn: alignNormal,
    params: {
      positionKey: 'y'
    }
  },
  b: {
    fn: alignNormal,
    params: {
      positionKey: 'y'
    }
  },
  lrc: {
    fn: alignCenter,
    params: {
      positionKey: 'x',
      minType: 'l',
      maxType: 'r'
    }
  },
  tbc: {
    fn: alignCenter,
    params: {
      positionKey: 'y',
      minType: 't',
      maxType: 'b'
    }
  },
  horizon: {
    fn: alignGap,
    params: {
      positionKey: 'x',
      lengthKey: 'width'
    }
  },
  vertical: {
    fn: alignGap,
    params: {
      positionKey: 'y',
      lengthKey: 'height'
    }
  }
};

export default function changeAlign(graph, type, selectItem) {
  const match = ALIGN_TYPE[type];

  // 重置记录
  updateModels = {};
  originModels = {};

  match.fn(graph, type, getNodes(selectItem), match.params);

  // 记录更改状态 有才改
  if (Object.keys(updateModels).length) {
    graph.execCmd('batchUpdate', {
      originModels: originModels,
      updateModels: updateModels
    });
  }
}

# 减少图元

图元的数量直接影响性能,所以需要尽可能的减少绘制的图元。

# 节点

  1. 排查发现,节点使用了多套尺寸的样式,与设计同步后决定去除小尺寸的样式。
  2. 有些显示项是可以控制显示的,所以不显示的改为动态绘制。

#

边同理,当前不显示的就不绘制,显示时才绘制。

# 优化内存

模拟5000+设备发现内存直接占用到了1.5G+,这是不能接受的。

排查发现每个节点都存储了其所有子节点的数据,相当于存了无数份树结构的数据在内存中。

改为节点只记录子节点的id,需要用时通过id去全局的唯一一份Map数据中取。

优化后1w+设备只占用了400M的内存。

# 杂项

# 文字超出宽度显示省略号

canvas中没法像css那样加几个属性就可以显示省略号了,所以这里得手动实现

# 思路

主要是英文算1px,中文算2px,用盒子宽度除以全英文得到最大显示字数,再循环文字,文字长度大于最大时就裁剪掉,拼上省略号。

# 实现

/**
 * 超出宽度的文字添加省略号
 * @param text
 * @param width
 * @param fontSize
 * @returns {string|*}
 */
export function addLongTextEllipsis(text, width, fontSize) {
  // 宽度除以字体大小,需乘以2才是全英文的最大长度,再减去3(省略号)减去2(左右加点间距)
  const maxLen = Math.floor(width / fontSize) * 2 - 3 - 2;
  if (text.length === 0 || maxLen <= 0) {
    return text;
  }
  let len = 0;
  let needPreCut = false;
  let i = 0;
  for (; i < text.length; i++) {
    if (text.charCodeAt(i) > 255) {
      len += 2;
    } else {
      len += 1;
    }
    if (len > maxLen) {
      needPreCut = true;
      break;
    }
  }
  if (needPreCut) {
    return text.substr(0, i) + '...';
  } else {
    return text;
  }
}